package com.fwtai.pay.wechat;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fwtai.pay.wechat.auth.PrivateKeySigner;
import com.fwtai.pay.wechat.auth.Verifier;
import com.fwtai.pay.wechat.auth.WechatPay2Credentials;
import com.fwtai.pay.wechat.auth.WechatPay2Validator;
import com.fwtai.pay.wechat.cert.CertificatesManager;
import com.fwtai.pay.wechat.util.AesUtil;
import com.fwtai.pay.wechat.util.PemUtil;
import com.fwtai.tool.IdBuilder;
import com.fwtai.tool.ToolOkHttp;
import com.fwtai.tool.ToolString;
import okhttp3.Response;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Base64;
import java.util.HashMap;

// apiV3 文档 https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient https://wechatpay-api.gitbook.io/wechatpay-api-v3/
/*
  todo 查看文档api的正确方式：
    关闭订单API,无返回值,API文档 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_3.shtml
    官方的只给请求的`请求URL`、`请求参数`、`返回参数`、`返回示例`、`请求示例`及`错误码`，剩余的自己脑补,然后就是下面的写法
    public static void closeOrder(final String out_trade_no){}
*/
//商户模式的支付工具类
public final class ToolWechat{

  private ToolWechat(){}

  //获取验证器
  private static Verifier getVerifier() throws Exception{
    final PrivateKey privateKey = PemUtil.loadPrivateKey(WechatPayConstants.PRIVATE_KEY);
    // 获取证书管理器实例
    final CertificatesManager certificatesManager = CertificatesManager.getInstance();
    // 向证书管理器增加需要自动更新平台证书的商户信息
    certificatesManager.putMerchant(
      WechatPayConstants.MCH_ID,
      new WechatPay2Credentials(WechatPayConstants.MCH_ID,new PrivateKeySigner(WechatPayConstants.MCH_SERIAL_NO,privateKey)),
      WechatPayConstants.API_V3KEY.getBytes(StandardCharsets.UTF_8));
    // 从证书管理器中获取verifier
    return certificatesManager.getVerifier(WechatPayConstants.MCH_ID);
  }

  //通知回调-验签
  public static boolean signVerify(final String serial,final String message,final String signature) throws Exception{
    return getVerifier().verify(serial,message.getBytes(StandardCharsets.UTF_8),signature);
  }

  //通知回调-解密
  public static String decrypt(final String body) throws JsonProcessingException, GeneralSecurityException{
    final AesUtil aesUtil = new AesUtil(WechatPayConstants.API_V3KEY.getBytes(StandardCharsets.UTF_8));
    final ObjectMapper objectMapper = new ObjectMapper();
    final JsonNode node = objectMapper.readTree(body);
    final JsonNode resource = node.get("resource");
    final String ciphertext = resource.get("ciphertext").asText();
    final String associatedData = resource.get("associated_data").asText();
    final String nonce = resource.get("nonce").asText();
    return aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),nonce.getBytes(StandardCharsets.UTF_8),ciphertext);
  }

  public static CloseableHttpClient getHttpClient() throws Exception{
    final Verifier verifier = getVerifier();
    final PrivateKey privateKey = PemUtil.loadPrivateKey(WechatPayConstants.PRIVATE_KEY);
    // 构造httpclient
    return WechatPayHttpClientBuilder.create().withMerchant(WechatPayConstants.MCH_ID, WechatPayConstants.MCH_SERIAL_NO,privateKey).withValidator(new WechatPay2Validator(verifier)).build();
  }

  //加密
  public static String sign(final byte[] message,final String private_key){
    try {
      final Signature sign = Signature.getInstance("SHA256withRSA");
      final PrivateKey privateKey = PemUtil.loadPrivateKey(private_key);
      sign.initSign(privateKey);
      sign.update(message);
      return Base64.getEncoder().encodeToString(sign.sign());
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
    } catch (SignatureException e) {
      throw new RuntimeException("签名计算失败", e);
    } catch (InvalidKeyException e) {
      throw new RuntimeException("无效的私钥", e);
    }
  }

  //获取‘预支付交易会话标识’,todo 官方的api文档只给：请求URL、请求参数、请求示例、返回参数、返回示例以及错误码，剩余的自己脑补（文档地址 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml）
  protected static String getPrepayId(final String out_trade_no,final String openId,final String notifyUrl,final Integer total,final String description,final String... attach) throws Exception{
    final HttpPost httpPost = getHttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi",getPreParam(out_trade_no,openId,notifyUrl,total,description,attach));
    final CloseableHttpResponse response = getHttpClient().execute(httpPost);
    return EntityUtils.toString(response.getEntity());
  }

  //生成支付参数
  protected static StringEntity getPreParam(final String out_trade_no,final String openId,final String notifyUrl,final Integer total,final String description,final String... attach) throws IOException{
    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    final ObjectMapper objectMapper = new ObjectMapper();
    final ObjectNode rootNode = objectMapper.createObjectNode();
    rootNode.put("mchid",WechatPayConstants.MCH_ID)
      .put("appid",WechatPayConstants.APP_ID)
      .put("description",description)
      .put("notify_url",notifyUrl)
      .put("attach",handlerArrs(attach))//附加数据(限制长度是128个),以逗号,隔开，在查询API和支付通知中原样返回，可作为自定义参数使用，实际情况下只有支付完成状态才会返回该字段。
      .put("out_trade_no",out_trade_no);
    rootNode.putObject("amount").put("total",total);//单位：分钱，需要x100才是64位的正整数
    rootNode.putObject("payer").put("openid",openId);
    objectMapper.writeValue(bos, rootNode);
    return new StringEntity(bos.toString("UTF-8"),StandardCharsets.UTF_8);
  }

  //将数组转字符串,以逗号隔开
  private static String handlerArrs(final String... array){
    final StringBuilder sb = new StringBuilder();
    // 遍历数组，将元素添加到StringBuilder对象中
    for (int i = 0; i < array.length; i++) {
      sb.append(array[i]);
      if (i < array.length - 1) {
        sb.append(",");
      }
    }
    return sb.toString();
  }

  /**
   * 初始化支付参数
  */
  protected static HashMap<String,String> initParam(final String timestamp,final String prepayValue){
    final HashMap<String,String> map = new HashMap<>(6);
    final String nonceStr = String.valueOf(new IdBuilder().getId121L());
    map.put("appId",WechatPayConstants.APP_ID);//若是服务商模式的话则在`服务商平台`下的‘产品中心’->‘AppID账号管理’->‘+关联AppID’ 关联即可（可以不是同一个企业名）；若是商户模式的话则是在`商户平台`的‘产品中心’->‘AppID账号管理’->‘+关联AppID’ 关联即可；
    map.put("timeStamp",timestamp);
    map.put("nonceStr",nonceStr);
    map.put("package","prepay_id="+prepayValue);//示例值：prepay_id=wx201410272009395522657a690389285100
    map.put("signType","RSA");//签名类型，默认为RSA，仅支持RSA
    map.put("paySign",getPaySign(timestamp,nonceStr,prepayValue));
    return map;
  }

  /**生成预支付参数,先判断是否为空再做业务处理*/
  public static HashMap<String,String> getPayParams(final String out_trade_no,final String openId,final String notifyUrl,final Integer total,final String body,final String... attach)throws Exception{
    final String prepayId = getPrepayId(out_trade_no,openId,notifyUrl,total,body,attach);
    final JsonNode node = new ObjectMapper().readTree(prepayId);
    final String prepayValue = node.get("prepay_id").asText();
    final String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
    return initParam(timestamp,prepayValue);
  }

  //签名方法,使用字段appId、timeStamp、nonceStr、package（示例值：prepay_id=wx201410272009395522657a690389285100）；小程序或app的签名是一致的,需要注意的是小程序的是 prepay_id=xxxxx，而app都没有prepay_id=，即直接就是 xxxxx
  public static String getPaySign(final String timestamp,final String nonceStr,final String prepayValue){
    final StringBuilder builder = new StringBuilder();
    //应用ID
    builder.append(WechatPayConstants.APP_ID).append("\n");//每行都要拼接\n换行符
    //时间戳
    builder.append(timestamp).append("\n");//每行都要拼接\n换行符
    //随机字符串（nonceStr）
    builder.append(nonceStr).append("\n");//每行都要拼接\n换行符
    //订单详情扩展字符串|预支付交易会话的id|prepay_id
    builder.append("prepay_id=").append(prepayValue).append("\n");//每行都要拼接\n换行符
    return sign(builder.toString().getBytes(StandardCharsets.UTF_8),WechatPayConstants.PRIVATE_KEY);
  }

  //关闭订单API,无返回值,API文档 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_3.shtml
  /**todo 官方的只给请求的`请求URL`、`请求参数`、`返回参数`、`返回示例`、`请求示例`及`错误码`，剩余的自己脑补,然后就是下面的写法*/
  public static void closeOrder(final String out_trade_no){
    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    final ObjectMapper objectMapper = new ObjectMapper();
    final ObjectNode rootNode = objectMapper.createObjectNode();
    rootNode.put("mchid",WechatPayConstants.MCH_ID);
    try {
      objectMapper.writeValue(bos, rootNode);
      final HttpPost httpPost = getHttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"+out_trade_no+"/close",new StringEntity(bos.toString("UTF-8"),StandardCharsets.UTF_8));
      final CloseableHttpResponse response = getHttpClient().execute(httpPost);//关闭订单是没有返回值
      final int result = response.getStatusLine().getStatusCode();
      System.out.println(result);
      //final String result = EntityUtils.toString(response.getEntity());//有返回值时
      //System.out.println(result);
    } catch (final Exception exception) {
      exception.printStackTrace();
    }
  }

  //微信支付订单号查询 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_2.shtml
  public static void getOrderByTransactionId(final String mchid,final String transaction_id) throws Exception{
    final HttpPost httpPost = getHttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/id/"+transaction_id+"?mchid="+mchid,null);
    final CloseableHttpResponse response = getHttpClient().execute(httpPost);
    final String result = EntityUtils.toString(response.getEntity());
    System.out.println(result);
  }

  //商户订单号查询 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_2.shtml
  public static void getOrderByOutTradeNo(final String mchid,final String out_trade_no) throws Exception{
    final HttpPost httpPost = getHttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"+out_trade_no+"?mchid="+mchid,null);
    final CloseableHttpResponse response = getHttpClient().execute(httpPost);
    final String result = EntityUtils.toString(response.getEntity());
    System.out.println(result);
  }

  /**获取微信服务器的支付成功通知参数,必须验证非空再业务处理*/
  public static String getNotifyInfo(final HttpServletRequest request) throws Exception{
    final BufferedReader br = request.getReader();//获取 应答主体（response Body），需要按照接口返回的顺序进行验签，错误的顺序将导致验签失败。
    String str = "";
    final StringBuilder builder = new StringBuilder();
    while((str = br.readLine()) != null){
      builder.append(str);
    }
    final String signStr = request.getHeader("Wechatpay-Timestamp") + "\n" + request.getHeader("Wechatpay-Nonce") + "\n" + builder + "\n";
    //1.验证签名 https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
    if(!signVerify(request.getHeader("Wechatpay-Serial"),signStr,request.getHeader("wechatpay-signature"))){
      return null;
    }
    //2.解密报文
    return decrypt(builder.toString());
  }

  //商户模式的获取通知参数,支付附加参数
  public static HashMap<String,String> extractParam(final String notifyInfo){
    final HashMap<String,String> map = ToolString.parseJsonObject(notifyInfo);
    final String openid = ToolString.parseJsonObject(map.get("payer")).get("openid");
    final String total_fee = ToolString.parseJsonObject(map.get("amount")).get("total");//total是总金额;payer_total是实际支付的金额.若实际支付的金额不等于总金额时需要后台处理
    final HashMap<String,String> params = new HashMap<>(4);
    params.put("out_trade_no",map.get("out_trade_no"));
    params.put("transaction_id",map.get("transaction_id"));
    params.put("total_fee",String.valueOf(Double.parseDouble(total_fee)/100));//支付时x100,此处要/100
    params.put("openid",openid);
    return params;
  }

  /**
   * 获取POST请求HttpPost
   * @param entity POST请求时不能为空
   * @作者 田应平
   * @QQ 444141300
   * @创建时间 2022-07-21 14:51:41
  */
  public static HttpPost getHttpPost(final String url,final HttpEntity entity){
    final HttpPost httpPost = new HttpPost(url);
    httpPost.addHeader("Accept", "application/json");
    httpPost.addHeader("Content-type","application/json; charset=utf-8");
    httpPost.setEntity(entity);
    return httpPost;
  }

  /**
   * 获取GET请求HttpGet
   * @作者 田应平
   * @QQ 444141300
   * @创建时间 2022/7/21 14:29
  */
  private static HttpGet getHttpGet(final String url){
    final HttpGet httpGet = new HttpGet(url);
    httpGet.addHeader("Accept", "application/json");
    httpGet.addHeader("Content-type","application/json; charset=utf-8");
    return httpGet;
  }

  //接收成功时应答,final
  public static String success(){
    return "{\"code\": \"SUCCESS\",\"message\": \"接收成功\"}";
  }

  //接收成功但处理业务失败时应答,final
  public static String failure(){
    return "{\"code\": \"FAIL\",\"message\": \"失败\"}";
  }

  //通过登录获取openid或和和session_key,仅需传递微信开发配置的appId及密钥可动态获取微信数据
  public static JSONObject getWechatInfo(final String code,final String appId,final String secret){
    final String grant_type = "authorization_code";
    final String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId + "&secret=" + secret + "&js_code=" + code + "&grant_type=" + grant_type;
    try {
      final Response response = ToolOkHttp.ajaxGet(url);//小程序登录
      if(response.body() == null) return null;
      return JSON.parseObject(response.body().string());
    } catch (final Exception e){
      e.printStackTrace();
    }
    return null;
  }

  //获通过动态令牌换取微信用户绑定的手机号,仅需传递微信开发配置的appId及密钥可动态配置效果
  public static JSONObject getPhoneNumber(final String code,final String appId,final String secret){
    try {
      final String access = getAccessToken(appId,secret);
      final String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token="+access+"&code="+code;
      final HashMap<String,Object> params = new HashMap<>(1);
      params.put("code",code);
      return JSON.parseObject(ToolOkHttp.postJsonString(url,params));
    } catch (final Exception e){
      e.printStackTrace();
    }
    return null;
  }

  //获取小程序 scheme 码，适用于短信、邮件、外部网页、微信内等拉起跳转小程序;errcode==0时是成功的,openlink就是要的参数
  //返回的结果 {"errcode":0,"openlink":"weixin://dl/business/?t=hQPgakdWkIn","errmsg":"ok"}
  public static JSONObject getUrlscheme(final String appId,final String secret){
    try {
      final String token = getAccessToken(appId,secret);
      final String url = "https://api.weixin.qq.com/wxa/generatescheme?access_token="+token;
      final HashMap<String,Object> params = new HashMap<>(2);
      final HashMap<String,Object> jump_wxa = new HashMap<>(1);
      params.put("path","/pages/home/home");//指定跳转到小程序的页面路径
      params.put("query",new IdBuilder().getId121L());
      jump_wxa.put("jump_wxa",params);
      return JSON.parseObject(ToolOkHttp.postJsonString(url,jump_wxa));
    } catch (final Exception e){
      e.printStackTrace();
    }
    return null;
  }

  //获取 access_token|ACCESS_TOKEN,指定appId和密钥
  public static String getAccessToken(final String appId,final String secret) throws IOException{
    final String access_token = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + secret;
    final Response res = ToolOkHttp.ajaxGet(access_token);
    if(res.body() == null) return null;
    return JSON.parseObject(res.body().string()).getString("access_token");
  }

  //获取不限制的小程序码
  public static String getAppletCode(final String filePath,final String scene){
    try {
      final String accessToken = getAccessToken(WechatPayConstants.APP_ID,WechatPayConstants.SECRET);
      final String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;
      final HashMap<String,Object> params = new HashMap<>(5);
      params.put("page","pages/verify-authenticity/verify-authenticity");
      params.put("scene",scene);
      params.put("check_path",true);
      params.put("env_version","develop");//release"，开发版为 develop
      params.put("width","280px");
      final byte[] bytes = ToolOkHttp.postJsonByte(url,params);
      final File file = new File(filePath);
      final FileOutputStream fos = new FileOutputStream(file);
      fos.write(bytes);
      fos.close();
      return file.getAbsolutePath();
    } catch (final Exception ignored){
      return null;
    }
  }

  public static byte[] getAppletCode(final String scene){
    try {
      final String accessToken = getAccessToken(WechatPayConstants.APP_ID,WechatPayConstants.SECRET);
      final String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;
      final HashMap<String,Object> params = new HashMap<>(5);
      params.put("page","pages/verify-authenticity/verify-authenticity");
      params.put("scene",scene);
      params.put("check_path",true);
      params.put("env_version","develop");//release"，体验版为 "trial"，开发版为
      params.put("width","280px");
      return ToolOkHttp.postJsonByte(url,params);
    } catch (final Exception ignored){
      return null;
    }
  }
}